package com.jse.server;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.System.Logger.Level;
import java.net.HttpCookie;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.jse.Errs;
import com.jse.Fs;
import com.jse.Http;
import com.jse.Jse;
import com.jse.Lang;
import com.jse.Log;
import com.jse.Times;
import com.jse.json.Json;
import com.jse.json.JsonObject;
import com.jse.json.XML;
import com.jse.web.Ctx;
import com.jse.web.Web;

public class HttpCtx extends JsonObject implements Ctx {
	
	private static final HashMap<String,Object> app=new HashMap<>();
	
	public Socket socket;
	private String method;
	private String sid;//session id
	private String path;
	private String url;
	private String mapping;
	private String suffix;
	private String query;//querystring
	private HashMap<String,Object> attrs=new HashMap<String, Object>();
	private HashMap<String,HttpCookie> cookies=new HashMap<>();
	HashMap<String,String> headers=new HashMap<>();
	private HashMap<String,String> resheaders=new HashMap<>();
	private Object session;
	private int status=200;
	private String msg="OK";
	private InputStream in;
	private OutputStream out;
	private boolean committed;
	private String ip;//ip
	private long time;
	private String body;//ip
	private boolean isWebSocket;
	private boolean close;//
	private boolean forward;
	BufferedReader br;
	
	public HttpCtx() {}
	
	public void parse(Object so) {
		this.socket=(Socket)so;
		time=System.currentTimeMillis();
		try {out=new ByteArrayOutputStream();
		in=socket.getInputStream();
			br = new BufferedReader(new InputStreamReader(in,StandardCharsets.ISO_8859_1));
	        int lineNum = 0;
	        String line = "";
	        while ((line = br.readLine()) != null) {
	            if ("".equals(line)) {//body
	                if(!isWebSocket) {
	                	parseBody(br);//请求体
	                }
	                break;
	            }
	            if (lineNum++ == 0) {//首行
	                boolean ishttp=parseFirstLine(line);
	                if(!ishttp)return;//不是http请求//准备下一行
	            } else {//请求头
	                parseHeader(line);
	            }
	        }
		} catch (IOException e) {
			e.printStackTrace();
		}
    }

    private boolean parseFirstLine(String line) {
    	String[] first = line.split("\\s");
    	if(first.length!=3) {
    		return false;
    	}
        method = first[0];
        parseUrl(first[1]);
		return true;
    }
    private void parseUrl(String uri) {
    	url = uri;//"首行第三个"+first[2]);//协议
        String[] qs=url.split("\\?");
        url=qs[0];
        path=url.indexOf('%')!=-1?URLDecoder.decode(url,StandardCharsets.UTF_8):url;
		suffix=Fs.suffix(qs[0]);
//		super.clear();
		if(qs.length>1) {
			query=qs[1];
			var ps=qs[1].split("&");
			for (int i = 0; i < ps.length; i++) {
				var px=ps[i].split("=");
				put(px[0],px[1]);
			}
		}
    }

    public boolean isWebsocket() {
        return isWebSocket;
    }
	
	@Override
	public Object req() {return null;}

	@Override
	public Object res() {return null;}

	@Override
	public JsonObject tbl(){return this;}

	@Override
	public String getMethod() {return method;}

	@Override
	public String getHeader(String name) {return headers.get(name);}

	@Override
	public void setAttribute(String name, Object value) {attrs.put(name, value);}

	@Override
	public Object getAttribute(String name) {return attrs.get(name);}

	@Override
	public Object getSession() {
		if(session==null) {
			if(sid==null) {
				var s=cookies.get("JSESSIONID");
				if(s==null) {
					sid=Lang.uuid();
				}
			}
			session=Web.SESSIONS.get(sid);
			if(session==null) {
				session=new Session(sid);
				Web.SESSIONS.put(sid,session);
				String cookieStr="JSESSIONID="+sid+"; Path=/; SameStrict; HttpOnly";
				resheaders.put("Set-Cookie",cookieStr);
			}
		}
		return session;
	}

	@Override
	public String getRequestedSessionId() {return ((Session)getSession()).getId();}

	@Override
	public Map<String, HttpCookie> cookies() {return cookies;}

	@Override
	public String reqcontentType() {
		return getHeader("content-type");
	}

	@Override
	public String getContentType() {
		return resheaders.get("Content-Type");
	}

	@Override
	public void setContentType(String type) {
		resheaders.put("Content-Type",type);
	}

	@Override
	public void setStatus(int status) {
		this.status=status;
	}

	@Override
	public void sendError(int status, String msg) {
		this.status=status;
		this.msg=msg;
	}

	@Override
	public OutputStream getResponseBody() {
		return out;
	}

	@Override
	public boolean isCommitted() {
		return committed;
	}

	@Override
	public void sattr(String name, Object value) {
		((Session)getSession()).setAttribute(name, value);
	}

	@Override
	public Object sattr(String name) {
		return ((Session)getSession()).getAttribute(name);
	}
	@Override public void sattrdel(String name) {((Session)getSession()).removeAttribute(name);}
	@Override public void invalidate() {((Session)getSession()).invalidate();}
	@Override public void app(String name, Object value) {
		app.put(name, value);
	}

	@Override
	public Object app(String name) {return app.get(name);}

	@Override
	public Map<String, Object> sessionMap() {
		return ((Session)getSession()).attributes();
	}

	@Override
	public void setHeader(String name, String value) {
		resheaders.put(name, value);
	}

	@Override
	public InputStream getRequestBody() {
		if(in==null) {
			ByteArrayOutputStream out=new ByteArrayOutputStream();
			try {
				while(br.ready()){
					out.write(br.read());}
				in=new ByteArrayInputStream(out.toByteArray());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return in;
	}

	@Override
	public Map<String, Object> attr() {return attrs;
	}

	@Override
	public int httpStatus() {
		return status;
	}
	
	@Override
	public void redirect(String url) {
		try {
			String line="""
					HTTP/1.1 %d %s
					Location: %s\r\n\r\n""".formatted(301,Http.msg(301),url);
			out.write(line.getBytes());
			out.flush();
		} catch (IOException e) {
			throw Errs.rt(e);
		}
		
	}

	@Override
	public void sendResponseHeaders(int rCode, long contentLen) throws IOException {
//		String header="""
//				HTTP/1.1 %d %s
//				Content-Type: %s\r\n
//				""".formatted(code,msg,getContentType());
//		out.write(header.getBytes());
//		out.flush();
	        this.status = rCode;//msg是http code msg
	        String statusLine = "HTTP/1.1 "+rCode+Http.msg(rCode)+"\r\n";
//	        OutputStream tmpout = new BufferedOutputStream (out);
//	        PlaceholderOutputStream o = getPlaceholderResponseBody();
	        out.write(statusLine.getBytes(), 0, statusLine.length());
	        boolean noContentToSend = false; // assume there is content
	        boolean noContentLengthHeader = false; // must not send Content-length is set
	        resheaders.put("Date", Times.format("HTTP",Instant.now()));

	        /* check for response type that is not allowed to send a body */

	        if ((rCode>=100 && rCode <200) /* informational */
	            ||(rCode == 204)           /* no content */
	            ||(rCode == 304))          /* not modified */
	        {
	            if (contentLen != -1) {
	                String msg = "sendResponseHeaders: rCode = "+ rCode
	                    + ": forcing contentLen = -1";
	                Log.log(Level.WARNING, msg);
	            }
	            contentLen = -1;
	            noContentLengthHeader = (rCode != 304);
	        }

	        if (method.equals("HEAD") || rCode == 304) {
	            /* HEAD requests or 304 responses should not set a content length by passing it
	             * through this API, but should instead manually set the required
	             * headers.*/
	            if (contentLen >= 0) {
	                String msg =
	                    "sendResponseHeaders: being invoked with a content length for a HEAD request";
	                Log.log (Level.WARNING, msg);
	            }
	            noContentToSend = true;
	            contentLen = 0;
	            out.write(rCode);
//	            o.setWrappedStream (new FixedLengthOutputStream (this, out, contentLen));
	        } else { /* not a HEAD request or 304 response */
	            if (contentLen == 0) {
	            	if (isWebSocket) {
//	                    o.setWrappedStream(new UndefLengthOutputStream(this, ros));
	                    close = true;
	                } else {
//	                    resheaders.put("Transfer-encoding", "chunked");//临时住宿
//	                    o.setWrappedStream(new ChunkedOutputStream(this, ros));
	                }
	            } else {
	                if (contentLen == -1) {
	                    noContentToSend = true;
	                    contentLen = 0;
	                }
	                if (!noContentLengthHeader) {
	                	resheaders.put("Content-length", Long.toString(contentLen));
	                }
//	                o.setWrappedStream (new FixedLengthOutputStream (this, out, contentLen));
	            }
	        }
	        if (!close) {
	            if ("close".equalsIgnoreCase(resheaders.get("connection"))) {
	                close = true;
	            }
	        }
	        
	        Set<java.util.Map.Entry<String, String>> entries = resheaders.entrySet();
	        for (var entry : entries) {
	            String key = entry.getKey();
	            String values = entry.getValue();
	            out.write((key+": "+values+"\r\n").getBytes());
//	            System.out.println(key+"=="+values);
	        }
	        out.write ('\r');
	        out.write ('\n');
//	        tmpout.flush() ;
//	        tmpout = null;
//	        resheaders.clear();
	        if (noContentToSend) {
	        	if (close) {
	                return;
	            }
	            close = true;
	        }
//	        out.flush();
	}

	@Override
	public String getRemoteAddr() {
		return ip;
	}

	@Override
	public String getServletPath() {
		return url;
	}

	@Override
	public String getMapping() {
		if(mapping==null)mapping=Web.mapping(url, suffix, tbl());
		return mapping;
	}

	@Override
	public String getQueryString() {return query;}
	@Override public String getSuffix() {
		return suffix;
	}
	@Override public String getServerName() {return getHeader("host");}
	@Override public long time() {return time;}
	@Override public String getPath(){return path;}
	@Override public void clear() {super.clear();attrs.clear();headers.clear();resheaders.clear();}
	@Override public Object socket() {return socket;}
	@Override public String body() {return body;}
	@Override public boolean isForward() {return forward;}
	

    private void parseHeader(String line) {
    	if(line.toLowerCase().startsWith("cookie:")){//cookie
    		String[] itemCookie = line.substring("cookie:".length()).replaceAll("\\s*", "").split(";");//去除全部空格分隔;
            for (String s : itemCookie)//key=value
            {
                final String cookieTag = "=";
                String key;
                String value;
                if (!s.contains(cookieTag))//特殊的不符合键值类型的cookie，通常将该内容存为键，值是true
                {
                    key = s;
                    value = String.valueOf(true);
                }
                else
                {
                    key = s.substring(0, s.indexOf(cookieTag));
                    value = s.substring(s.indexOf(cookieTag) + cookieTag.length());
                }
                if("JSESSIONID".equals(key))sid=value;
                cookies.put(key,new HttpCookie(key, value));
            }
            if(!cookies.containsKey("JSESSIONID")){sid=Lang.uuid();}
    	}else {
        	var ss=line.split(": ");
        	String name=ss[0].toLowerCase();
        	String value=ss[1];
        	headers.put(name,value);
        	if(name.contains("upgrade")&&"websocket".equalsIgnoreCase(value)) {
        		isWebSocket=true;
        	}
    	}
    }

    private void parseBody(BufferedReader br) throws IOException {
    	  if("POST".equals(method)) {
        	String reqcontentType=reqcontentType();
        	if(reqcontentType==null||reqcontentType.startsWith("application/x-www-form-urlencoded")) {
        		StringBuilder sb=new StringBuilder();
            	while (br.ready())sb.append((char)br.read());
            	if(!sb.isEmpty()) {
            		var ps=sb.toString().split("&");
            		for (int i = 0; i < ps.length; i++) {
						var px=ps[i].split("=");
						if(px.length==2) {
							var val=new String(px[1].getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
							put(px[0],val);
						}else put(px[0],"");
					}
            	}
            }else if(reqcontentType.startsWith("application/json")) {
            	StringBuilder sb=new StringBuilder();
            	while (br.ready())sb.append((char)br.read());
            	putAll(Json.jsonObject(new String(sb.toString().getBytes(StandardCharsets.ISO_8859_1),
            			StandardCharsets.UTF_8)));
            }else if(reqcontentType.startsWith("application/xml")) {
            	StringBuilder sb=new StringBuilder();
            	while (br.ready())sb.append((char)br.read());
            	putAll(XML.toJSONObject(new String(sb.toString().getBytes(StandardCharsets.ISO_8859_1),
            			StandardCharsets.UTF_8)));
            }else if(reqcontentType.startsWith("multipart/form-data")) {//上传
            	var boundary=reqcontentType.substring(30);
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                while(br.ready()) {
                	output.write(br.read());
                }
                String upload="/upload/"+Jse.name+"/"+Times.upymd()+"/";
                var pp=Path.of(Jse.webapps(),upload);
                if(!Files.exists(pp))Files.createDirectories(pp);
            	MultipartProcessor.process(boundary,new ByteArrayInputStream(output.toByteArray()),(part)->{
            		if(part.getFilename()==null) {
            			put(part.getName(),part.readBodyText());
            		}else {
            			String filename = upload+part.getFilename();
    			        Fs.write(Jse.webapps()+filename, part.getBodyStream());
    			        put(part.getName(),filename);
            		}
                });
            }else {
            	StringBuilder sb=new StringBuilder();
            	while (br.ready())sb.append((char)br.read());
            	put("body",new String(sb.toString().getBytes(StandardCharsets.ISO_8859_1),
            			StandardCharsets.UTF_8));
            }
        }
        
    }

	@Override
	public void forward(String url) {
		try {
			forward=true;
			this.path=url;
			this.suffix=Fs.suffix(path);
			parseUrl(url);
			Web.service(this);
		} catch (IOException e) {
			throw Errs.rt(e);
		}
	}

	@Override public void attr(String name, Object value) {attrs.put(name,value);}
	@Override public Object attr(String name) {return attrs.get(name);}

	
}